package framework.ctx
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;

	import framework.Utils;

	import mx.core.IFactory;
	import mx.core.UIComponent;
	import mx.core.mx_internal;
	import mx.events.FlexEvent;

	import org.as3commons.lang.ClassUtils;
	import org.as3commons.lang.ObjectUtils;
	import org.as3commons.lang.StringUtils;
	import org.as3commons.reflect.Field;
	import org.as3commons.reflect.MetaData;
	import org.as3commons.reflect.Type;

	use namespace mx_internal;

	public class CtxCore extends EventDispatcher implements IHierarchicalCtx, IConfigurableCtx,
		IUIHook
	{
		public function CtxCore()
		{
			register(this, "c", true);
		}

		public var beans:Object = {};

		private var _ctxConfiguration:CtxConfiguration = new CtxConfiguration;

		private var _parentCtx:ICtx;

		public function containsBean(clazzOrId:Object):Boolean
		{
			if (clazzOrId is String)
			{
				return beans[clazzOrId.toString()] !== undefined;
			}
			else
			{
				var clazz:Class = clazzOrId as Class
				for (var s:String in beans)
				{
					var bd:BeanDefinition = beans[s];
					if (ClassUtils.isAssignableFrom(clazz, bd.factoryClass))
					{
						return true;
					}
				}
				return false;
			}
		}

		public function get ctxConfiguration():CtxConfiguration
		{
			return _ctxConfiguration;
		}

		public function set ctxConfiguration(v:CtxConfiguration):void
		{
			_ctxConfiguration = v;
		}

		public function disableUIHook():void
		{
			UIComponent.mx_internal::dispatchEventHook = null;
		}

		public function enableUIHook():void
		{
			UIComponent.mx_internal::dispatchEventHook = dispatchEventHook;
		}

		public function fetchBean(clazz:Class, idToCreate:String = null):*
		{
			var bean:* = getBean(clazz);
			if (!containsBean(clazz))
			{
				register(clazz, idToCreate);
			}
			return getBean(clazz);
		}

		private function getBeanForWiring(clazzOrId:Object, requestingBean:BeanDefinition):*
		{
			var r:* = getBean(clazzOrId);
			if (!r)
			{
				warn("No bean returned for " + clazzOrId + " Requesting bean: " +
					 requestingBean.factoryClass + " " + requestingBean.beanId);
			}
			return r;
		}

		public function getBean(clazzOrId:Object):*
		{
			var bds:Array = getBeanDefinitions(clazzOrId);
			if (bds.length > 1)
				throw new Error("Found more than one matching instances for " + clazzOrId);
			if (bds.length == 0)
			{
				if (_parentCtx)
				{
					return _parentCtx.getBean(clazzOrId);
				}
				else
					return undefined;
			}
			var bd:BeanDefinition = bds[0];
			if (bd.initializing)
			{
				warn("Circular dependency for bean " + bd.beanId);
				return undefined;
			}
			if (bd.instance)
			{
				if (!bd.completlyWired)
				{
					wireBean(bd, bd.instance);
				}
				return bd.instance;
			}
			bd.initializing = true;
			var instance:* = bd.notReadyInstance;
			if (!instance)
			{
				if (bd.factoryMethodName)
				{
					instance = Function(bd.factoryClass[bd.factoryMethodName]).apply(this, bd.
																					 factoryMethodArgs);
				}
				else
				{
					var clazz:Class = bd.factoryClass;
					instance = new clazz;
				}
				bd.notReadyInstance = instance;
			}
			if (!bd.isResource)
			{
				wireBean(bd, instance);
				if (!bd.requiredWired)
				{
					bd.initializing = false;
					return undefined;
				}
				initBean(bd, instance);
			}
			else
			{
				bd.completlyWired = true;
				bd.requiredWired = true;
			}
			bd.instance = instance;
			bd.initializing = false;
			return instance;
		}

		public function getBeanDefinitions(clazzOrId:Object):Array
		{
			if (clazzOrId is String)
			{
				if (!containsBean(clazzOrId))
					return [];
				return [ beans[clazzOrId.toString()]];
			}
			else
			{
				var r:Array = [];
				var clazz:Class = clazzOrId as Class;
				if (isClassSimple(clazz))
				{
					warn("Simple type " + clazz + "is used to query beans. No beans will be returned. Use ID instead. ");
					return [];
				}
				for (var s:String in beans)
				{
					var bd:BeanDefinition = beans[s];
					if (ClassUtils.isAssignableFrom(clazz, bd.factoryClass))
					{
						r.push(bd);
					}
				}
				return r;
			}
		}

		public function getBeans():Object
		{
			return beans;
		}

		public function getIdsOfType(clazz:Class):Array
		{
			var r:Array = [];
			for (var s:String in beans)
			{
				var bd:BeanDefinition = beans[s];
				if (ClassUtils.isAssignableFrom(clazz, bd.factoryClass))
				{
					r.push(bd.beanId);
				}
			}
			return r;
		}

		public function get parentCtx():ICtx
		{
			return _parentCtx;
		}

		public function set parentCtx(v:ICtx):void
		{
			this._parentCtx = v;
		}

		public function register(instanceOrClass:Object, id:String = null, resource:Boolean =
								 false):BeanDefinition
		{
			var bd:BeanDefinition = new BeanDefinition;
			bd.autowire = _ctxConfiguration.autowire;
			bd.required = _ctxConfiguration.required;
			bd.wireById = _ctxConfiguration.wireById;
			bd.wireByType = _ctxConfiguration.wireByType;
			bd.autoCreate = _ctxConfiguration.autoCreate;
			bd.isResource = resource;
			if (ObjectUtils.isSimple(instanceOrClass))
			{
				bd.isResource = true;
			}
			if (instanceOrClass is IFactory)
			{
				bd.isSingleton = false;
				bd.factoryMethodName = "newInstance";
			}
			var type:Type;
			if (instanceOrClass is Class)
			{
				var clazz:Class = instanceOrClass as Class;
				type = Type.forClass(clazz);
			}
			else
			{
				bd.notReadyInstance = instanceOrClass;
				type = Type.forInstance(instanceOrClass);
			}
			if (!id)
			{
				id = StringUtils.uncapitalize(type.name);
			}
			bd.beanId = id;
			bd.factoryClass = type.clazz;
			bd.type = type;
			var md:MetaData = findBasePropertiesAndMetadata(bd, type.metaData);
			if (md)
			{
				if (md.hasArgumentWithKey("scope"))
				{
					bd.isSingleton = md.getArgument("scope").value == "singleton";
				}
				if (md.hasArgumentWithKey("id"))
				{
					bd.beanId = md.getArgument("id").value;
				}
				if (md.hasArgumentWithKey("resource"))
				{
					bd.isResource = md.getArgument("resource").value == "true";
				}
				if (md.hasArgumentWithKey("initialize"))
				{
					bd.initialize = md.getArgument("initialize").value == "true";
				}
			}
			return registerBeanDefinition(bd);
		}

		public function registerBeanDefinition(beanDefinition:BeanDefinition):BeanDefinition
		{
			beans[beanDefinition.beanId] = beanDefinition;
			Utils.firePropertyUpdate(this, beanDefinition.beanId);
			checkBeansForReadiness();
			return beanDefinition;
		}

		public function unregister(id:String):void
		{
			delete beans[id];
		}

		protected function dispatchEventHook(e:Event, uiComponent:UIComponent):void
		{
			if (e.type == FlexEvent.PREINITIALIZE && uiComponent is IUIBean)
			{
				var id:String = "ui." + uiComponent.name;
				register(uiComponent, id);
				getBean(id); //to initialize
			}
		}

		private function checkBeansForReadiness():void
		{
			for each (var bd:BeanDefinition in beans)
			{
				if (!bd.isResource && !bd.completlyWired && bd.notReadyInstance)
				{
					wireBean(bd, bd.notReadyInstance);
					if (bd.lastWiresCount)
						Utils.callLater(Utils.firePropertyUpdate, [ this, bd.beanId ]);
				}
			}
		}

		private function findBasePropertiesAndMetadata(baseConfig:BaseConfig, mts:Array):MetaData
		{
			for each (var md:MetaData in mts)
			{
				if (_ctxConfiguration.metatagAliases.indexOf(md.name) != -1)
				{
					baseConfig.autowire = true;
					if (md.hasArgumentWithKey("byId"))
					{
						baseConfig.wireById = md.getArgument("byId").value == "true";
					}
					if (md.hasArgumentWithKey("byType"))
					{
						baseConfig.wireByType = md.getArgument("byType").value ==
							"true";
					}
					if (md.hasArgumentWithKey("enabled"))
					{
						baseConfig.autowire = md.getArgument("enabled").value ==
							"true";
					}
					if (md.hasArgumentWithKey("required"))
					{
						baseConfig.required = md.getArgument("required").value ==
							"true";
					}
					if (md.hasArgumentWithKey("autoCreate"))
					{
						baseConfig.autoCreate = md.getArgument("autoCreate").value ==
							"true";
					}
					return md;
				}
			}
			return null;
		}

		private function initBean(bd:BeanDefinition, instance:*):void
		{
			if (!bd.initialize)
				return;
			if ("initBean" in instance && instance["initBean"] is Function)
			{
				instance["initBean"]();
			}
			if (instance is IEventDispatcher)
			{
				IEventDispatcher(instance).dispatchEvent(new Event(Event.INIT));
			}
		}

		private function isClassSimple(clazz:Class):Boolean
		{
			return clazz == Number || clazz == String || clazz == int || clazz ==
				Boolean || clazz == Class || clazz == Function || clazz == Array ||
				clazz == Object || clazz == Dictionary;
		}

		private function listProperties(bd:BeanDefinition, instance:*):Array
		{
			var r:Array = [];
			for each (var field:Field in bd.type.fields)
			{
				if (_ctxConfiguration.ignoreProperties.indexOf(field.name) != -1)
					continue;
				var pd:PropertyDefinition = new PropertyDefinition;
				pd.propName = field.name;
				pd.clazz = field.type.clazz;
				pd.autowire = bd.autowire;
				pd.required = bd.required;
				pd.wireById = bd.wireById;
				pd.wireByType = bd.wireByType;
				pd.autoCreate = bd.autoCreate;
				//by default, disable autowiring for fields with "_" (for older version beans)
				if (pd.propName.indexOf("_") == 0)
					pd.autowire = false;
				//by default, disable autowiring by type for beans of simple types
				if (isClassSimple(field.type.clazz))
				{
					pd.wireByType = false;
				}

				var md:MetaData = findBasePropertiesAndMetadata(pd, field.metaData);
				if (pd.autowire)
				{
					if (md && md.hasArgumentWithKey("id"))
					{
						pd.id = md.getArgument("id").value;
						pd.wireById = true;
					}
					if (!pd.id)
					{
						pd.id = pd.propName;
					}
					r.push(pd);
				}
			}
			return r;
		}

		private function warn(msg:String):void
		{
			if (!ctxConfiguration.warningsDisabled)
			{
				trace(msg);
			}
		}

		private function wireBean(bd:BeanDefinition, instance:*):void
		{
			var props:Array = bd.props;
			if (!props)
			{
				props = listProperties(bd, instance);
				bd.props = props;
			}
			var completlyWired:Boolean = true;
			var requiredWired:Boolean = true;
			bd.lastWiresCount = 0;
			for each (var pd:PropertyDefinition in props)
			{
				if (pd.wired)
				{
					continue;
				}
				var bean:* = undefined;
				if (pd.wireById)
				{
					bean = getBeanForWiring(pd.id, bd);
				}
				if (!bean && pd.wireByType)
				{
					bean = getBeanForWiring(pd.clazz, bd);
				}
				if (bean === undefined && pd.autoCreate)
				{
					register(pd.clazz, pd.id);
					bean = getBeanForWiring(pd.clazz, bd);
				}
				if (bean === undefined)
				{
					if (pd.required)
					{
						warn("Bean [" + bd.beanId + "]  Required field " + pd.propName +
							 " cannot be wired by id (" + pd.wireById + ") or by type (" +
							 pd.wireByType + ") to bean: id=" + pd.id + " class: " +
							 pd.clazz);
						requiredWired = false;
					}
					completlyWired = false;
					continue;
				}
				instance[pd.propName] = bean;
				pd.wired = true;
				bd.lastWiresCount++;
			}
			bd.completlyWired = completlyWired;
			bd.requiredWired = requiredWired;
		}
	}
}